Mise à jour le 17/11/2021
Prototype

Prototype

1. Introduction

Le Prototype est un design pattern très simplement accessible en PHP puisque l'usage de la méthode __clone est une application de celui-ci.

Le Prototype est la capacité d'un Object de se cloner en un nouvel Object qui hérite de l'état de l'Object cloné.

A noter que le __clone crée une nouvelle référence pour l'Object copié.

Dans le cas où un Object fait référence à d'autres Objects (par exemple un Attribute "Color $color" ), cette référence est copiée, ce qui peut être problématique si l'on modifie la valeur de l'Object référencé.

2. Le problème

Supposons qu'on a une Class Pencil qui aurait un Attribute Color :

Pencil.php
<?php
class Pencil implements PencilInterface
{
    protected ColorInterface $color;

    public function __construct(ColorInterface $color) 
    {
        $this->color = $color;
    }

    public function getColor(): ColorInterface 
    {
        return $this->color;
    }
}

PencilInterface.php
<?php
interface PencilInterface 
{
    public function getColor(): ColorInterface;
}

ColorInterface.php
<?php
interface ColorInterface 
{
    public function getValue(): string;
}

Color.php
<?php
class Color implements ColorInterface 
{
    public function __construct($value)
    {
        $this->value = $value;
    }

    public function setValue(string $value)
    {
        $this->value = $value;
    }

    public function getValue(): string
    {
        return $this->value;
    }
}


Voici un comportement problématique :

    $color = new Color('red');
    $firstPencil = new Pencil($color);
    $clonedPencil = clone $firstPencil;
    $color->setValue('green');
    echo $clonedPencil->getColor()->getValue(); // -> 'green'
    echo $firstPencil->getColor()->getValue(); // -> 'green'


Ici, les deux instances de Pencil se retrouvent avec une couleur ayant la valeur verte. Ce que l'on ne souhaitait pas forcément : on souhaite avoir un Pencil ayant une Color 'green' et l'autre ayant une Color 'red'.
La seule façon de résoudre ce problème est de surcharger la méthode __clone afin de cloner également l'instance de Color (et donc obtenir une nouvelle référence différente de la première).

3. Solution

Il faut réécrire la méthode __clone de cette façon :

Pencil.php
<?php
class Pencil implements PencilInterface
{
    protected ColorInterface $color;

    public function __construct(ColorInterface $color) 
    {
        $this->color = $color;
    }

    public function getColor(): ColorInterface 
    {
        return $this->color;
    }

    public function __clone() 
    {
        $this->color = clone $this->color;
    }
}


Se faisant, si l'on réexecute le bout de code précédent, on obtient ceci

    $color = new Color('red');
    $firstPencil = new Pencil($color);
    $clonedPencil = clone $firstPencil;
    $color->setValue('green');
    echo $clonedPencil->getColor()->getValue(); // -> 'red'
    echo $firstPencil->getColor()->getValue(); // -> 'green'

Le premier crayon a bien hérité d'une couleur verte, tandis que le second a bien une couleur qui a été modifiée en rouge.

4. Les ValueObjects

Le ValueObject est un Object immutable, c'est à dire que ces propriétés sont initialisés qu'à un seul moment, la grande majorité du temps dans le constructeur ; il est aussi possible de faire des setters qui ne settent qu'une seule fois la valeur, ce qui peut potentiellement produire un effet de bord selon le test effectué.

5. Setter d'un immutable

Voici à quoi pourrait ressembler un tel setter. Reprenons la Class Pencil :
<?php
class Pencil implements PencilInterface
{
protected ?ColorInterface $color = null;

public function __construct(ColorInterface $color)
{
$this->color = $color;
}


public function setColor(?ColorInterface $color = null): PencilInterface
{
if (\is_null($this->color)) {
$this->color = $color;
}

return $this;
}

public function getColor(): ColorInterface
{
return $this->color;
}
}


Ici, on sette la couleur une seule et unique fois, mais étant donné que l'Attribute color peut également être null, il est tout à fait possible de passer une première fois dans la méthode, setter $color à null, puis repasser une autre fois et setter $color avec une instance implémentant ColorInterface.
Le principe d'un Object immutable est de ne plus être modifié après sa création, ce qui potentiellement peut toujours être le cas ici.
Eventuellement, on pourrait rajouter un Attribute indiquant si l'état est fixe ou non, mais cela deviendrait compliqué à gérer s'il fallait identifier pour chaque Attribute si son setter a été ou non appelé. En outre, il faudrait également garantir que cet Attribute n'est setté qu'une seule fois.

Donc la solution de passer par des setters plutôt que par le constructeur n'est pas une bonne idée.

6. Comment cloner un Object possédant un Attribute ValueObject ?

Il est difficile de lier les concepts de Prototype et de ValueObject car il faudrait créer une méthode clone ayant des paramètres similaire à ceux du __construct.
Dans le cas de l'Attribute Color, si l'on considère que c'est un ValueObject, il ne serait plus possible de modifier la Color du Pencil cloné, ce qui serait déroutant.

Donc si l'on souhaite cloner un Object possédant un Attribute ValueObject, il faut accepter que cet Attribute sera immutable d'un clone à l'autre.

7. Factory+Pool

Pour créer un Pencil avec une Color immutable, on peut passer par une Factory, voire mélanger le pattern design Pool et Factory afin de conserver les différents Pencil créés.

On peut envisager de n'avoir qu'une seule Factory nommé PencilFactory (on pourrait éventuellement avoir une deuxième Factory nommée ColorFactory qui stockerait les différents instances de Color).

Ici, la PencilPoolFactory garde en mémoire les différentes instance de Color créées puis fabrique et retourne des instances de Pencil. Etant donné qu'il change d'état, il n'est donc pas possible de passer par une méthode statique.

<?php
class PencilPoolFactory
{
    /**
    * ColorInterface[]
    */
    protected array $colors = array();

    public function has(string $key)
    {
        return isset($this->colors[$key]);
    }

    public function get(string $colorValue): ColorInterface
    {
        return $this->colors[$colorValue];
    }
  
     public function add(ColorInterface $color, string $colorValue)
     {
         $this->colors[$colorValue] = $color;
     }

    public function create(string $colorValue)
    {
        if (!$this->has($colorValue)) {  
            $this->add(new Color($colorValue), $colorValue);
        }

        return new Pencil($this->get($colorValue));
    }
}


Voici comment l'utiliser :

    $pencilPoolFactory = new PencilPoolFactory();
    $redPencil = $pencilPoolFactory->create('red');
    $greenPencil = $pencilPoolFactory->create('green');

    echo $redPencil->getColor()->getValue(); // 'red'
    echo $greenPencil->getColor()->getValue(); // 'red'


🧙‍♂️️Le PencilPoolFactory mélange deux design patterns mais cela produit un mauvais pattern (le mot antipattern est un peu trop populaire et désigne aujourd'hui des choses eux-mêmes populaires).
D'une première raison : ce PencilPoolFactory fabrique des instances de Color ET des instances de Pencil, il a donc deux responsabilités, ce n'est pas la faute de la combinaison des deux design patterns.
D'une deuxième raison : le PencilPoolFactory garde en mémoire les instances de Color, ce qui lui donne une nouvelle responsabilité car l'utilisateur sera tenté de faire également un $pencilPoolFactory->get('color').


7.1 Résolution du problème

Il faut découper le PencilPoolFactory en trois : PencilFactory, ColorFactory et PencilPool. Chacune de ces Classs aura une seule responsabilité.

7.1.1 PencilPool avec un Attribute $colorFactory

* Le PencilFactory aura un Attribute Singleton : $pencilPool.
* Le PencilPool aura un Attribute Singleton : $colorFactory.

PencilFactory.php
<?php
class PencilFactory
{
    PencilPoolInterface $pencilPool;

    public function __construct(
        PencilPoolInterface $pencilPool
    ) 
    {
        $this->pencilPool = $pencilPool
    }

    public function create(string $colorValue): PencilInterface
    {
        $color = $this->pencilPool->get($colorValue);

        return new Pencil($color);
    }

    protected function getColor(string $colorValue): ColorInterface
    {
        return $this->pencilPool->get($colorValue);
    }
}



Cette solution est dérangeante. Le PencilPool dispose d'un Attribute autre que celui pour lequel il est dédié, ce qui est déroutant car il stocke des références de ColorInterface ET de PencilFactoryInterface.

La solution inverse semble plus pertinente : le PencilFactory possède un Attribute $pencilPool qui ferait office de stockage des nouvelles instances.

7.1.2 ColorFactory avec un Attribute $pencilPool

Le PencilPool se réduit ici à son strict nécessaire : il stocke des instances implémentant ColorInterface

PencilPool.php
<?php
class PencilPool implements PencilPoolInterface
{
    /**
    * ColorInterface[]
    */
    protected array $colors = array();

    public function has(string $key)
    {
        return isset($this->colors[$key]);
    }

    public function get(string $colorValue): ColorInterface
    {
        return $this->colors[$colorValue];
    }
  
    public function add(ColorInterface $color, string $colorValue)
    {
        $this->colors[$colorValue] = $color;
    }
}

Et le PencilFactory possède un Attribute $pencilPool.

PencilFactory.php
<?php
class PencilFactory implements PencilFactoryInterface
{
    protected PencilPool $pencilPool;
    
    public function create(string $colorValue)
    {
        if (!($pencilPool->has($colorValue)) {
            $color = new Color($colorValue);
            $pencilPool->set($color, $colorValue);
        } else {
            $color = $pencilPool->get($colorValue);
        }

        return $color;
    }
}

La PencilFactory demande à sa $pencilPool si la Color existe, auquel cas elle le retourne. Si ce n'est pas le cas, elle la crée et le transmet au pencilPool pour que ce dernier le stocke.

Eventuellement, on pourrait imaginer une nouvelle Class plus complexe, avec un nom un peu ambigue comme PencilManager.

Cette Class ferait le lien entre le PencilFactory, le ColorFactory et le PencilPool.
Cela garantirait que les trois Class seraient simples et immutables (on ne les modifierait plus une fois conçues).

Sauf qu'on imagine bien que le PencilManager finirait par avoir plusieurs responsabilités, même si elles sont déléguées à d'autres Class, ce qui n'est en soi pas un problème si on fait en sorte que ce Manager ne représente que la fusion des trois autres.
Ce ne serait qu'une Facade, qui ne devrait faire qu'une chose : retourner un Pencil en fonction d'une valeur de couleur.